package com.apexcode.judge.service.impl;

import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.apexcode.common.core.constants.Constants;
import com.apexcode.common.core.enums.CodeRunStatus;
import com.apexcode.common.core.constants.JudgeConstants;
import com.apexcode.judge.callback.DockerStartResultCallback;
import com.apexcode.judge.callback.StatisticsCallback;
import com.apexcode.judge.domain.CompileResult;
import com.apexcode.judge.domain.SandBoxExecuteResult;
import com.apexcode.judge.service.ISandboxService;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.*;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Image;
import com.github.dockerjava.api.model.Volume;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.netty.NettyDockerCmdExecFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @ClassName SandboxServiceImpl
 * @Description 实现代码沙箱服务，通过 Docker 容器编译和运行用户提交的 Java 代码。
 * @Author ZJX
 * @Date 2025/3/31 16:33
 * @Version 1.0
 **/

@Service
@Slf4j
public class SandboxServiceImpl implements ISandboxService {

    @Value("${sandbox.docker.host:tcp://localhost:2375}")
    private String dockerHost;

    @Value("${sandbox.limit.memory:100000000}")
    private Long memoryLimit;

    @Value("${sandbox.limit.memory-swap:100000000}")
    private Long memorySwapLimit;

    @Value("${sandbox.limit.cpu:1}")
    private Long cpuLimit;

    @Value("${sandbox.limit.time:5}")
    private Long timeLimit;

    private DockerClient dockerClient;

    private String containerId;

    private String userCodeDir;

    private String userCodeFileName;

    /**
     * 编译并执行用户提交的 Java 代码。
     *
     * @param userId   用户 ID，用于标识代码所属用户
     * @param userCode 用户提交的 Java 代码字符串
     * @param inputList 输入用例列表，每个用例用于测试代码
     * @return SandBoxExecuteResult 执行结果，包含状态、输出、资源使用信息等
     */
    @Override
    public SandBoxExecuteResult exeJavaCode(Long userId, String userCode, List<String> inputList) {
        createUserCodeFile(userId, userCode); // 创建并保存用户代码文件
        initDockerSanBox(); // 初始化 Docker 沙箱环境

        // 编译代码
        CompileResult compileResult = compileCodeByDocker();
        if (!compileResult.isCompiled()) {
            deleteContainer(); // 编译失败，清理容器
            deleteUserCodeFile(); // 清理代码文件
            return SandBoxExecuteResult.fail(CodeRunStatus.COMPILE_FAILED, compileResult.getExeMessage()); // 返回编译失败结果
        }

        // 执行代码
        return executeJavaCodeByDocker(inputList); // 执行并返回结果
    }

    /**
     * 创建并保存用户提交的 Java 代码到本地文件系统。
     *
     * @param userId   用户 ID，用于生成唯一目录
     * @param userCode 用户提交的 Java 代码字符串
     */
    private void createUserCodeFile(Long userId, String userCode) {
        String examCodeDir = System.getProperty("user.dir") + File.separator + JudgeConstants.EXAM_CODE_DIR;
        if (!FileUtil.exist(examCodeDir)) {
            FileUtil.mkdir(examCodeDir); // 如果目录不存在，创建目录
        }
        String time = LocalDateTimeUtil.format(LocalDateTime.now(), DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); // 生成时间戳
        // 拼接用户代码文件路径，使用用户 ID 和时间戳确保唯一性
        userCodeDir = examCodeDir + File.separator + userId + Constants.UNDERLINE_SEPARATOR + time;
        userCodeFileName = userCodeDir + File.separator + JudgeConstants.USER_CODE_JAVA_CLASS_NAME;
        FileUtil.writeString(userCode, userCodeFileName, Constants.UTF8); // 将代码写入文件，使用 UTF-8 编码
    }

    /**
     * 初始化 Docker 沙箱环境，包括配置客户端、拉取镜像和创建/启动容器。
     */
    private void initDockerSanBox() {
        DefaultDockerClientConfig clientConfig = DefaultDockerClientConfig.createDefaultConfigBuilder()
                .withDockerHost(dockerHost) // 配置 Docker 主机地址
                .build();
        dockerClient = DockerClientBuilder
                .getInstance(clientConfig)
                .withDockerCmdExecFactory(new NettyDockerCmdExecFactory()) // 使用 Netty 工厂创建客户端
                .build();
        // 拉取 Java 运行时镜像
        pullJavaEnvImage();
        // 创建容器并设置资源限制和权限
        HostConfig hostConfig = getHostConfig();
        CreateContainerCmd containerCmd = dockerClient
                .createContainerCmd(JudgeConstants.JAVA_ENV_IMAGE) // 指定 Java 镜像
                .withName(JudgeConstants.JAVA_CONTAINER_NAME); // 设置容器名称
        CreateContainerResponse createContainerResponse = containerCmd
                .withHostConfig(hostConfig) // 应用资源和权限配置
                .withAttachStderr(true) // 捕获标准错误输出
                .withAttachStdout(true) // 捕获标准输出
                .withTty(true) // 分配伪终端
                .exec();
        // 记录容器 ID
        containerId = createContainerResponse.getId();
        // 启动容器
        dockerClient.startContainerCmd(containerId).exec();
    }

    /**
     * 拉取 Java 运行时镜像，如果已存在则跳过。
     */
    private void pullJavaEnvImage() {
        ListImagesCmd listImagesCmd = dockerClient.listImagesCmd();
        List<Image> imageList = listImagesCmd.exec(); // 获取所有镜像列表
        for (Image image : imageList) {
            String[] repoTags = image.getRepoTags();
            if (repoTags != null && repoTags.length > 0 && JudgeConstants.JAVA_ENV_IMAGE.equals(repoTags[0])) { // 检查是否已存在指定镜像
                return;
            }
        }
        PullImageCmd pullImageCmd = dockerClient.pullImageCmd(JudgeConstants.JAVA_ENV_IMAGE); // 创建拉取命令
        try {
            pullImageCmd.exec(new PullImageResultCallback()).awaitCompletion(); // 异步拉取镜像并等待完成
        } catch (InterruptedException e) {
            throw new RuntimeException("拉取镜像失败", e);
        }
    }

    /**
     * 配置 Docker 容器的资源限制和权限。
     *
     * @return HostConfig 配置对象
     */
    private HostConfig getHostConfig() {
        HostConfig hostConfig = new HostConfig();
        // 设置挂载目录，将用户代码路径映射到容器
        hostConfig.setBinds(new Bind(userCodeDir, new Volume(JudgeConstants.DOCKER_USER_CODE_DIR))); // 挂载本地代码到容器
        // 限制 Docker 容器资源
        hostConfig.withMemory(memoryLimit); // 设置内存限制
        hostConfig.withMemorySwap(memorySwapLimit); // 设置交换内存限制
        hostConfig.withCpuCount(cpuLimit); // 设置 CPU 核心数
        hostConfig.withNetworkMode("none");  // 禁用网络，增强安全
        hostConfig.withReadonlyRootfs(true); // 禁止在根目录写文件，防止恶意操作
        return hostConfig;
    }

    /**
     * 在 Docker 容器中编译 Java 代码。
     *
     * @return CompileResult 编译结果，包含是否成功和错误信息
     */
    private CompileResult compileCodeByDocker() {
        String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVAC_CMD, null, containerId); // 创建编译命令 ID
        DockerStartResultCallback resultCallback = new DockerStartResultCallback(); // 初始化回调对象
        CompileResult compileResult = new CompileResult();
        try {
            dockerClient.execStartCmd(cmdId)
                    .exec(resultCallback) // 执行编译命令
                    .awaitCompletion(); // 等待完成
            if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) { // 检查执行状态
                compileResult.setCompiled(false); // 设置编译失败
                compileResult.setExeMessage(resultCallback.getErrorMessage()); // 记录错误信息
            } else {
                compileResult.setCompiled(true); // 设置编译成功
            }
            return compileResult;
        } catch (InterruptedException e) {
            throw new RuntimeException("编译代码失败", e);
        }
    }

    /**
     * 在 Docker 容器中执行编译后的 Java 代码，处理多个输入用例。
     *
     * @param inputList 输入用例列表
     * @return SandBoxExecuteResult 执行结果，包含状态、输出、资源使用信息等
     */
    private SandBoxExecuteResult executeJavaCodeByDocker(List<String> inputList) {
        List<String> outList = new ArrayList<>(); // 初始化输出结果列表
        long maxMemory = 0L;  // 初始化最大内存使用量
        long maxUseTime = 0L; // 初始化最大运行时间
        // 执行用户代码
        for (String inputArgs : inputList) {
            String cmdId = createExecCmd(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArgs, containerId); // 创建执行命令 ID
            // 执行代码并开始计时
            StopWatch stopWatch = new StopWatch();
            // 启动资源监控
            StatsCmd statsCmd = dockerClient.statsCmd(containerId); // 开始监控容器资源
            StatisticsCallback statisticsCallback = statsCmd.exec(new StatisticsCallback()); // 初始化统计回调
            stopWatch.start(); // 开始计时
            DockerStartResultCallback resultCallback = new DockerStartResultCallback();
            try {
                dockerClient.execStartCmd(cmdId)
                        .exec(resultCallback) // 执行代码
                        .awaitCompletion(timeLimit, TimeUnit.SECONDS); // 限制执行时间
                if (CodeRunStatus.FAILED.equals(resultCallback.getCodeRunStatus())) { // 检查执行是否失败
                    // 未通过所有用例返回失败结果
                    return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED);
                }
            } catch (InterruptedException e) {
                throw new RuntimeException("执行代码失败", e);
            }
            stopWatch.stop();  // 结束计时
            statsCmd.close();  // 关闭资源监控
            long userTime = stopWatch.getLastTaskTimeMillis(); // 获取本次执行时间
            maxUseTime = Math.max(userTime, maxUseTime);       // 更新最大执行时间
            Long memory = statisticsCallback.getMaxMemory(); // 获取最大内存使用
            if (memory != null) {
                maxMemory = Math.max(maxMemory, statisticsCallback.getMaxMemory()); // 更新最大内存使用
            }
            outList.add(resultCallback.getMessage().trim());   // 记录输出结果，去除首尾空白
        }
        deleteContainer(); // 删除容器资源
        deleteUserCodeFile(); // 清理本地文件

        return getSanBoxResult(inputList, outList, maxMemory, maxUseTime); // 返回最终结果
    }

    /**
     * 创建并返回 Docker 容器中的执行命令 ID。
     *
     * @param javaCmdArr 命令数组（如编译或运行命令）
     * @param inputArgs  输入参数（可为 null）
     * @param containerId 容器 ID
     * @return String 执行命令的 ID
     */
    private String createExecCmd(String[] javaCmdArr, String inputArgs, String containerId) {
        if (!StrUtil.isEmpty(inputArgs)) {
            // 当输入参数不为空时，拼接入参
            String[] inputArray = inputArgs.split(" "); // 将输入参数按空格分割
            javaCmdArr = ArrayUtil.append(JudgeConstants.DOCKER_JAVA_EXEC_CMD, inputArray); // 追加输入参数到命令
        }
        ExecCreateCmdResponse cmdResponse = dockerClient.execCreateCmd(containerId)
                .withCmd(javaCmdArr) // 设置执行命令
                .withAttachStderr(true) // 捕获标准错误
                .withAttachStdin(true) // 允许标准输入
                .withAttachStdout(true) // 捕获标准输出
                .exec();
        return cmdResponse.getId(); // 返回命令 ID
    }

    /**
     * 封装并返回沙箱执行结果。
     *
     * @param inputList 输入用例列表
     * @param outList 输出结果列表
     * @param maxMemory 最大内存使用量
     * @param maxUseTime 最大运行时间
     * @return SandBoxExecuteResult 执行结果
     */
    private SandBoxExecuteResult getSanBoxResult(List<String> inputList, List<String> outList,
                                                 long maxMemory, long maxUseTime) {
        if (inputList.size() != outList.size()) {
            // 输入用例数量不等于输出结果数量，执行异常
            return SandBoxExecuteResult.fail(CodeRunStatus.NOT_ALL_PASSED, outList, maxMemory, maxUseTime); // 返回失败结果
        }
        return SandBoxExecuteResult.success(CodeRunStatus.SUCCEED, outList, maxMemory, maxUseTime); // 返回成功结果
    }

    /**
     * 删除 Docker 容器并关闭客户端连接。
     */
    private void deleteContainer() {
        // 执行完成后删除容器
        dockerClient.stopContainerCmd(containerId).exec(); // 停止容器
        dockerClient.removeContainerCmd(containerId).exec(); // 删除容器
        // 断开与 Docker 的连接
        try {
            dockerClient.close(); // 关闭 Docker 客户端
        } catch (IOException e) {
            throw new RuntimeException("删除容器失败", e);
        }
    }

    /**
     * 删除本地用户代码文件和目录。
     */
    private void deleteUserCodeFile() {
        FileUtil.del(userCodeDir); // 删除用户代码目录及其内容
    }

}